// ==UserScript==
// @name         小红书千帆助手
// @namespace    http://tampermonkey.net/
// @version      1.1.3
// @description  小红书千帆助手，自用。自动上新或者更新SKU图片、价格、库存。
// @author       You
// @match        https://ark.xiaohongshu.com/app-item/good/edit/*
// @grant        GM_addStyle
// @grant        GM_setValue
// @grant        GM_getValue
// @require      https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js
// @require      https://cdnjs.cloudflare.com/ajax/libs/layui/2.12.0/layui.js
// ==/UserScript==

(function() {

    // 第一个产品SKU图片选择器
    const firstItemSelector = 'div.new-sku-table div.d-table__content > table tr:first-child td:nth-child(3) img';
    // 替换按钮选择器
    const replaceBtnSelector = 'div.d-popover > div.upload-trigger__operas > div.trigger-opera__item:nth-child(2)';
    //图片选择器列表选择器
    const updateImageListSelector = 'div.drawer-main-right div.material-images-wrapper div.d-grid div.material-image-card div.image-info';
    // 图片选择器关闭按钮选择器
    const closeBtnSelector = 'div.d-drawer .d-space span.d-icon.d-drawer-close svg';
    // 图片选择器确认按钮选择器
    const confirmBtnSelector = '.d-drawer-footer > .d-space > .d-space> .--color-bg-primary';
    // SKU表格选择器
    const skuTableSelector = 'div.new-sku-table div.d-table__content > table tr';
    // 颜色分类选择器
    const skuTableByColorSelector = '.variant-info-main .d-grid > div';
    // 第一个产品SKU图片选择器（颜色分类）
    const firstItemByColorSelector = '.variant-info-main .d-grid > div:first-child div.upload-list img';
    // 最后一个商品规格输入框
    const lastProductSpecInputEl = '.variant-info-main > div.d-grid div.d-input-wrapper:first-child div.d-input > input';

    // 规格大图
    const specBigImgCbxEl = 'div.variant-info-header input[type="checkbox"]';

    let price = '1.1';
    let stock = '100';
    // 特殊价格和库存的SKU列表
    let specSKU = [];

    readConfig();

    // 引入css
    let link = document.createElement("link");
    link.rel = "stylesheet";
    link.href = "https://cdnjs.cloudflare.com/ajax/libs/layui/2.12.0/css/layui.css";
    document.head.appendChild(link);
    // layui.layer.msg('引入成功')
    GM_addStyle(`
        .layui-fixbar li {
            transition: transform 0.3s ease;
        }
        .layui-form-label {
            width: 100px;
        }
        .layui-input-block {
            margin-left: 100px;
        }
    `);

    var layer;
    var form;

    layui.use(function() {
        let util = layui.util;
        layer = layui.layer;
        form = layui.form;

        // 固定条是否展开
        let isShow = true;

        util.fixbar({
            default: false,
            bars: [
                {
                    type: 'open',
                    icon: 'layui-icon-folder-open',
                    style: 'border-radius: 25px; background-color: #EECAD6',
                },
                {
                    type: 'add',
                    icon: 'layui-icon-addition',
                    style: 'border-radius: 25px; background-color: #EECAD6;',
                },
                {
                    type: 'update',
                    icon: 'layui-icon-upload-drag',
                    style: 'border-radius: 25px; background-color: #EECAD6',
                },
                {
                    type: 'set',
                    icon: 'layui-icon-set',
                    style: 'border-radius: 25px; background-color: #EECAD6',
                },
                {
                    type: 'show',
                    icon: 'layui-icon-down',
                    style: 'border-radius: 25px; background-color: #EECAD6',
                },
            ],
            on: {
                click: async function(type) {
                    switch (type) {
                        case 'show':
                            // 将前三个fixbar隐藏
                            $('.layui-fixbar li').each(function() {
                                let type = $(this).attr('lay-type');
                                if (isShow) {
                                    if (type === 'show') {
                                        // 将图标旋转180°
                                        $(this).css({
                                            'transform': 'rotate(180deg)',
                                        })
                                    } else {
                                        $(this).fadeOut(300);
                                    }
                                } else {
                                    if (type === 'show') {
                                        // 将图标旋转180°
                                        $(this).css({
                                            'transform': 'unset',
                                        })
                                    } else {
                                        $(this).fadeIn(300);
                                    }
                                }
                            })
                            isShow = !isShow;
                            break;
                        case 'update':
                            let index = layer.confirm('价格：' + price + "，库存：" + stock + "。是否设置特殊价格和库存？", {icon: 3}, function () {
                                layer.close(index);
                                specStockAndPrice().then((res) => {
                                    if (res) {
                                        if (isOpenBigImg()) {
                                            updateProductByColor();
                                        } else {
                                            updateProduct();
                                        }
                                    }
                                })
                            }, function () {
                                if (isOpenBigImg()) {
                                    updateProductByColor();
                                } else {
                                    updateProduct();
                                }
                            })
                            break;
                        case 'open':
                            if (isOpenBigImg()) {
                                openImgSelector($(firstItemByColorSelector)[0]);
                            } else {
                                openImgSelector($(firstItemSelector)[0]);
                            }
                            break;
                        case 'add':
                            let idx = layer.confirm('价格：' + price + "，库存：" + stock + "。是否设置特殊价格和库存？", {icon: 3}, function () {
                                layer.close(idx);
                                specStockAndPrice().then((res) => {
                                    if (res) {
                                        addProductSpec();
                                    }
                                })
                            }, function () {
                                addProductSpec();
                            })

                            break;
                        case 'set':
                            configSet();
                            break;
                    }
                }
            }
        })
    })

    /**
     * 配置信息设置
     * @returns {Promise<void>}
     */
    async function configSet() {
        let formHTML = `
            <form class="layui-form" style="padding: 10px;">
                <div class="layui-form-item">
                    <label class="layui-form-label">售价</label>
                    <div class="layui-input-block">
                          <input type="text" class="layui-input" lay-affix="number" name="price" value="${price}">
                    </div>
                </div>
                <div class="layui-form-item">
                    <label class="layui-form-label">库存</label>
                    <div class="layui-input-block">
                          <input type="text" class="layui-input" lay-affix="number" name="stock" value="${stock}">
                    </div>
                </div>
            </form>
        `;

        let res = await layerAsync({
            type: 1,
            title: '参数设置',
            content: formHTML,
            shadeClose: true,
            btn: ['确定', '取消'],
            success: function (layero, index, that) {
                layui.form.render($(layero).find('form.layui-form'));
            },
            yes: function (index, layero, that) {
                // 获取输入的内容
                let $layero = $(layero);
                price = $layero.find('input[name="price"]').val();
                stock = $layero.find('input[name="stock"]').val();
                console.log(price, stock);
                saveConfig();

                layer.msg('保存成功', {icon: 1});

                return true;
            },
            cancel: function (index, layero, that) {
                return true;
            },
            btn2: function (index, layero, that) {
                return true;
            }
        })
    }

    /**
     * 保存配置信息到油猴
     */
    function saveConfig() {
        GM_setValue("price", price);
        GM_setValue("stock", stock);
    }

    /**
     * 从油猴读取配置信息
     */
    function readConfig() {
        price = Number(GM_getValue("price", 39.9));
        stock = Number(GM_getValue("stock", 1));
    }

    /**
     * 添加商品规格
     */
    async function addProductSpec() {
        // 判断是否开启了规格大图
        let selector = null;
        if (isOpenBigImg()) {
            selector = firstItemByColorSelector;
        } else {
            selector = firstItemSelector;
        }
        if (!selector) {
            layer.msg('未找到图片选择入口', {icon: 2});
            return false;
        }

        let ids = await getUpdateList(selector);
        console.log('新增SKU列表:', ids);
        for (let i = 0; i < ids.length; i++) {
            let productSpec = ids[i];
            let $inputs = $(lastProductSpecInputEl);
            if ($inputs.length <= 0) {
                layer.msg('未找到规格输入框', {icon: 2});
                continue;
            }
            let el = $inputs[$inputs.length - 1];

            await scrollToElement(el);

            inputVal(el, productSpec);

            await sleep(100);

            if (isOpenBigImg()) {
                // 如果开启了规格大图，那么需要点击后面的图标，上传图片
                let $prev = $(el).closest('.compact-wrapper').parent().prev();
                let $svg = $prev.find('.upload-list svg').parent();
                if ($svg.length <= 0) {
                    layer.msg('未找到上传图标', {icon: 2});
                    continue;
                }
                await selectImage($svg[0], productSpec);
            }
        }

        await updatePriceAndStock(ids);

        layer.alert('新增完成');
    }

    /**
     * 判断是否开启了规格大图
     * @returns {*|boolean|boolean}
     */
    function isOpenBigImg() {
        let $cbx = $(specBigImgCbxEl);
        if ($cbx.length <= 0) {
            return false;
        }
        return $cbx.is(':checked');
    }

    function layerAsync(options) {
        return new Promise(resolve => {
            layer.open({
                ...options,
                yes: function (index, layero, that) {
                    if (options.yes) {
                        if (options.yes(index, layero, that) === false) {
                            return;
                        }
                    }
                    layer.close(index);
                    return resolve('yes');
                },
                cancel: function (index, layero, that) {
                    if (options.cancel) {
                        if (options.cancel(index, layero, that) === false) {
                            return;
                        }
                    }
                    layer.close(index);
                    return resolve('cancel');
                },
                btn2: function (index, layero, that) {
                    if (options.btn2) {
                        if (options.btn2(index, layero, that) === false) {
                            return;
                        }
                    }
                    layer.close(index);
                    return resolve('cancel');
                },
            })
        })
    }

    /**
     * 异步提示框
     * @param options layer弹窗配置
     * @param callback 返回true时，才会继续执行
     * @returns {Promise<unknown>}
     */
    function promptAsync(options, callback) {
        return new Promise((resolve) => {
            layer.prompt(options, function (text, index) {
                let res = callback(text, index);
                if (res === true) {
                    resolve(res);
                }
            })
        })
    }

    /**
     * 更新产品（款式）
     * @returns {Promise<void>}
     */
    async function updateProductByColor() {
        console.log('开始更新产品（颜色分类）');

        let updateIds = await getUpdateList(firstItemByColorSelector);
        console.log('更新列表:', updateIds);

        // 更新图片
        // scrollToElement($(skuTableByColorSelector)[0]);
        const divs = Array.from(document.querySelectorAll(skuTableByColorSelector));
        for (const item of divs) {
            console.log('查看颜色分类:', item);
            await scrollToElement(item);
            let $img = $(item).find('img');
            if ($img.length <= 0) {
                console.warn('颜色分类没有图片');
                break;
            }

            // 获取SKU名称
            let skuName = $(item).find('div.compact-wrapper > div:first-child input').val().trim();
            console.log('当前查看skuName:', skuName);
            if (updateIds.includes(skuName)) {
                console.log('开始更新sku的图片', skuName);
                await selectImage($img[0], skuName);
            }
        }

        await updatePriceAndStock(updateIds);

        layer.alert('更新完成');
    }

    /**
     * 更新产品（款式）
     * @returns {Promise<void>}
     */
    async function updateProduct() {
        console.log('开始更新产品');

        let updateIds = await getUpdateList(firstItemSelector);
        console.log('更新列表:', updateIds);

        await updatePriceAndStock(updateIds);

        layer.alert('更新完成');
    }

    /**
     * 更新价格与库存
     * @param ids
     * @returns {Promise<void>}
     */
    async function updatePriceAndStock(ids) {
        if (ids.length <= 0) {
            return;
        }
        await loopSKUTable(async (skuName, $img, $priceInput, $stockInput) => {
            if (ids.includes(skuName)) {
                console.log('开始更新价格与库存:', skuName, $img);
                if (!isOpenBigImg()) {
                    await selectImage($img[0], skuName);
                    await sleep(200);
                }

                let finalPrice = price;
                let finalStock = stock;
                for (const sku of specSKU) {
                    if (sku.id === skuName) {
                        finalPrice = sku.price;
                        finalStock = sku.stock;
                        break;
                    }
                }

                // 更新价格和库存
                inputVal($priceInput[0], finalPrice);
                inputVal($stockInput[0], finalStock);
            }
        });
    }

    async function selectImage(imgEl, skuName) {
        // 打开图片选择器
        await openImgSelector(imgEl);

        await sleep(500);

        // 等待弹窗加载完成
        let el = await waitElement('div.d-drawer-content div.combine-search input');

        // 输入搜索条件
        inputVal(el, skuName);
        await sleep(100);
        // 点击搜索
        $('div.d-drawer-content div.combine-search button')[0].dispatchEvent(new MouseEvent('click', { bubbles: true }))
        await sleep(400);

        await loopImgList(async (item) => {
            let itemName = $(item).find('span').text().trim();
            let match = itemName.match(/(\d+)\.\w+/);
            if (!match) {
                return false;
            }
            itemName = match[1];

            // console.log('选择图片:', itemName, skuName, itemName === skuName);
            if (itemName === skuName) {
                // 滚动到图片
                await scrollToElement(item);
                // 点击图片
                $(item).click();
                await sleep(500);
                // 点击确认按钮
                $(confirmBtnSelector).click();
                // 返回true表示找到匹配项
                return true;
            }
        });
    }

    /**
     * 遍历SKU表格的每一行
     */
    async function loopSKUTable(callback) {
        // 已经遍历的款式列表
        let hasScanSKUs = [];

        // 是否遍历过新的款式
        let hasScanNew = true;

        let $tableDiv = $(skuTableSelector).closest('.d-table__content');
        let height = 0;
        await scrollToElement($tableDiv[0]);

        // 将表格滚动到第一行，初始化
        // 滚动表格
        console.log('滚动表格到第一行')
        $tableDiv[0].scrollTo({
            top: 0,
            behavior: 'smooth',
        })
        await sleep(500);

        // 查询售价与库存的列索引
        let priceIndex = 3;
        let stockIndex = 4;
        let imageIndex = 2;
        $tableDiv.find('th').each((index, item) => {
            let text = $(item).find('span:first-child').text().trim();
            console.log('查询列索引:', text, index);
            if (text === '售价') {
                priceIndex = index;
            } else if (text === '库存') {
                stockIndex = index;
            } else if (text === '改后库存') {
                stockIndex = index;
            } else if (text === '规格图') {
                imageIndex = index;
            }
        })

        while (hasScanNew) {
            hasScanNew = false;

            // 滚动表格
            $tableDiv[0].scrollTo({
                top: $tableDiv[0].scrollTop + height,
                behavior: 'smooth',
            })
            height += 500;

            await sleep(1000);

            const rows = Array.from(document.querySelectorAll(skuTableSelector));

            for (let i = 0; i < rows.length; i++) {
                const item = rows[i];

                // 检测元素是否还在DOM中
                if (!document.contains(item)) {
                    console.log('元素不存在');
                    break;
                }

                // 款式名称
                let name = $(item).find('td:first-child span:first-child').text().trim();

                // 检查是否已经遍历过
                if (hasScanSKUs.includes(name)) {
                    continue;
                }
                hasScanSKUs.push(name);
                hasScanNew = true;

                // 滚动到当前SKU位置
                await scrollToElement(item);
                // 售价输入框
                let $priceInput = $(item).find(`td:nth-child(${priceIndex + 1}) input`);
                // 库存输入框
                let $stockInput = $(item).find(`td:nth-child(${stockIndex + 1}) input`);
                // 图片元素
                let $img = $(item).find(`td:nth-child(${imageIndex + 1}) img`);
                if ($img.length <= 0) {
                    $img = $(item).find(`td:nth-child(${imageIndex + 1}) svg`);
                }

                console.log('款式名称:', name, '售价:', $priceInput.val(), '库存:', $stockInput.val());
                // 调用回调函数
                await callback(name, $img, $priceInput, $stockInput);
            }

            console.log('是否继续遍历:', hasScanNew);
        }
    }

    /**
     * 打开图片选择器
     */
    async function openImgSelector(el) {
        if (!el) {

            // 测试用
            // $('#mainSpec-guide-step-id2 > div.d-switch.d-clickable.d-switch-small.d-inline-block > div > div > span').click();
            // await sleep(500);


            let $firstItem = $(await waitElement(firstItemSelector));
            console.log('第一个产品SKU:', $firstItem);
            if ($firstItem.length <= 0) {
                console.log('没有获取到SKU');
                return;
            }

            el = $firstItem[0];
        }

        // 滚动到第一个SKU图片位置
        await scrollToElement(el);

        // 先尝试点击一次，看是否弹出图片选择框
        // 点击
        el.dispatchEvent(new MouseEvent('click', { bubbles: true }));
        await sleep(200);

        if ($(confirmBtnSelector).length <= 0) {
            // 触发悬浮
            hoverReplaceBtn(el);

            await sleep(100);

            // 点击替换按钮
            await clickReplaceBtn();
        }
    }

    /**
     * 获取需要更新的SKU列表
     * @returns {Promise<*[]>}
     */
    async function getUpdateList(el) {
        let $firstItem = $(await waitElement(el));
        console.log('第一个产品SKU:', $firstItem);
        if ($firstItem.length <= 0) {
            console.log('没有获取到SKU');
            return;
        }

        let firstImg = $firstItem[0];

        // 滚动到第一个SKU图片位置
        await scrollToElement(firstImg);

        // 触发悬浮
        hoverReplaceBtn(firstImg);

        // 点击替换按钮
        await clickReplaceBtn();

        // 循环图片列表
        let updateIds = [];
        await loopImgList((item) => {
            let id = $(item).find('span').text().trim();

            // 正则匹配数字
            let match = id.match(/(\d+)\.\w+/);
            if (match) {
                id = match[1];
            }

            if (id && updateIds.indexOf(id) === -1) {
                updateIds.push(id);
            }
        })

        // 关闭图片选择器
        closeImgSelector();

        // 解除悬浮
        unhoverReplaceBtn(firstImg);

        await sleep(500);

        return updateIds;
    }

    /**
     * 关闭图片选择器
     */
    function closeImgSelector() {
        console.log('开始关闭图片选择器', $(closeBtnSelector));
        let el = document.querySelector(closeBtnSelector);
        if (el) {
            el.dispatchEvent(new MouseEvent('click', { bubbles: true }));
        }
    }

    /**
     * 循环图片列表
     * @param callback 回调函数，参数为当前图片元素
     */
    async function loopImgList(callback) {
        // 等待列表出现后再遍历
        await waitElement(updateImageListSelector);

        const rows = Array.from(document.querySelectorAll(updateImageListSelector));
        for (const item of rows) {
            const result = await callback(item);
            if (result === true) {
                break;
            }
        }
    }

    /**
     * 滚动到元素位置
     */
     async function scrollToElement(el) {
        el.scrollIntoView({
            behavior: 'smooth',
            block: 'center',
        })
        await sleep(500);
    }

    /**
     * 触发悬浮操作
     * @param el
     */
    function hoverReplaceBtn(el) {
        el.dispatchEvent(new MouseEvent('mouseenter', { bubbles: true }));
    }

    /**
     * 解除悬浮操作
     */
     function unhoverReplaceBtn(el) {
        el.dispatchEvent(new MouseEvent('mouseleave', { bubbles: true }));
    }

    /**
     * 点击替换按钮
     */
     async function clickReplaceBtn() {
        let $btnReplace = $(await waitElement(replaceBtnSelector));
        if ($btnReplace.length > 0) {
            $btnReplace.click();
            return true;
        } else {
            console.error('没有获取到替换按钮');
            return false;
        }
    }

    /**
     * 等待元素出现
     * @param selector 元素选择器
     * @param times 最大等待次数
     * @param timeout 每次检查间隔时间（毫秒）
     * @returns {Promise<unknown>}
     */
    function waitElement(selector, times = 10, timeout = 200) {
        return new Promise((resolve) => {
            let count = 0;

            function check() {
                const el = document.querySelector(selector);
                if (el) {
                    resolve(el);
                    return;
                }

                if (count >= times) {
                    resolve(null); // 找不到
                    return;
                }

                count++;
                setTimeout(check, timeout);
            }

            check();
        });
    }

    /**
     * 等待指定时间
     * @param ms 等待时间（毫秒）
     * @returns {Promise<void>}
     */
    function sleep(ms) {
        return new Promise(resolve => setTimeout(resolve, ms));
    }

    /**
     * 输入值并触发input事件
     * @param el
     * @param val
     */
    function inputVal(el, val) {
        console.log('输入值:', val);
        el.dispatchEvent(new MouseEvent('focus', { bubbles: true }));
        $(el).val(val);
        el.dispatchEvent(new MouseEvent('input', { bubbles: true }));
        el.dispatchEvent(new MouseEvent('change', { bubbles: true }));
        el.dispatchEvent(new MouseEvent('blur', { bubbles: true }));
    }

    /**
     * 设置特殊的库存和价格
     */
    async function specStockAndPrice() {
        specSKU = [];

        let formHTML = `
            <form class="layui-form" style="padding: 10px;">
                <div class="layui-row">
                    <div class="layui-col-sm3">
                        <div class="layui-form-item">
                            <label class="layui-form-label">序号</label>
                            <div class="layui-input-block">
                                <input type="text" class="layui-input" lay-affix="number" name="id" value="" onkeyup="specStockEnter(this, event)">
                            </div>
                        </div>
                    </div>
                    <div class="layui-col-sm4">
                        <div class="layui-form-item">
                            <label class="layui-form-label">售价</label>
                            <div class="layui-input-block">
                                <input type="text" class="layui-input" lay-affix="number" name="price" value="${price}" onkeyup="specStockEnter(this, event)">
                            </div>
                        </div>
                    </div>
                    <div class="layui-col-sm4">
                        <div class="layui-form-item">
                            <label class="layui-form-label">库存</label>
                            <div class="layui-input-block">
                                <input type="text" class="layui-input" lay-affix="number" name="stock" value="${stock}" onkeyup="specStockEnter(this, event)">
                            </div>
                        </div>
                    </div>
                    <div class="layui-col-sm1">
                        <div class="layui-form-item" style="display: flex; align-items: center; justify-content: flex-end;height: 37px;">
                            <a class="layui-btn layui-btn-radius layui-btn-xs" onclick="addLine(this)"><i class="layui-icon layui-icon-addition"></i></a>
                            <a class="layui-btn layui-bg-red layui-btn-radius layui-btn-xs" onclick="delLine(this)"><i class="layui-icon layui-icon-subtraction"></i></a>
                        </div>
                    </div>
                </div>
            </form>
        `;

        let res = await layerAsync({
            type: 1,
            title: '特殊价格和库存设置',
            content: formHTML,
            shadeClose: true,
            btn: ['确定', '取消'],
            success: function (layero, index, that) {
                layui.form.render($(layero).find('form.layui-form'));
                $(layero).find('input[name="id"]').focus();
            },
            yes: function (index, layero, that) {
                // 获取输入的内容
                let $layero = $(layero);

                let isAllSuccess = true;
                $layero.find('.layui-row').each(function () {
                    let id = $(this).find('input[name="id"]').val();
                    let price = $(this).find('input[name="price"]').val();
                    let stock = $(this).find('input[name="stock"]').val();

                    if (!id || !price || !stock) {
                        isAllSuccess = false;
                        return;
                    }

                    specSKU.push({
                        id: id,
                        price: price,
                        stock: stock
                    })
                })

                if (!isAllSuccess) {
                    layer.msg('请填写完整信息', {icon: 2});
                    specSKU = [];
                    return false;
                }

                return true;
            },
            cancel: function (index, layero, that) {
                return true;
            },
            btn2: function (index, layero, that) {
                return true;
            }
        })

        console.log('特殊价格与库存：', specSKU);

        return res === 'yes';
    }

    function specStockEnter(el, event) {
        if (event.keyCode === 13) {
            event.preventDefault();

            console.log('按下了Enter键');
            addLine(el);
        }
    }

    function addLine(el) {
        console.log('添加一行', el);
        var $row = $(el.closest('.layui-row'));
        let $newRow = $row.clone();
        $newRow.find('input[name="id"]').val('');
        $newRow.find('input[name="price"]').val(price);
        $newRow.find('input[name="stock"]').val(stock);
        $row.after($newRow);

        // 聚焦到新添加的行的第一个输入框
        $newRow.find('input[name="id"]').focus();
    }

    function delLine(el) {
        console.log('删除一行', el);
        var $row = el.closest('.layui-row');
        $row.remove();
    }

    // 将函数暴露在全局window中，以便在HTML中调用
    unsafeWindow.addLine = addLine;
    unsafeWindow.delLine = delLine;
    unsafeWindow.specStockEnter = specStockEnter;

})();